8장. 제어문
지금까지는 코드를 위에서 아래로 한 줄씩만 실행했다. 하지만 실제 프로그램은 조건에 따라 갈라지고, 같은 일을 여러 번 반복하기도 한다.
이 장에서는 흐름을 바꾸는 도구들을 익힌다.
목표:
- 조건문
if로 분기를 만들 수 있다 switch로 여러 경우를 깔끔히 나눌 수 있다for한 가지로 모든 반복을 표현할 수 있다break,continue, 라벨로 흐름을 세밀하게 제어한다
8.1 조건문 if / else
가장 기본적인 분기 도구다. 조건이 참일 때 블록 안의 코드를 실행한다.
기본 문법
if 조건 {
// 조건이 참일 때
} else if 다른조건 {
// 위는 거짓이고 이건 참일 때
} else {
// 모두 거짓일 때
}
간단한 예제로 살펴본다.
package main
import "fmt"
func main() {
score := 85
if score >= 90 {
fmt.Println("A")
} else if score >= 80 {
fmt.Println("B")
} else if score >= 70 {
fmt.Println("C")
} else {
fmt.Println("F")
}
}
실행 결과:
B
조건에 괄호를 쓰지 않는다
C, Java 와 다르게 조건 자체를 괄호로 감싸지 않는다.
// OK
if x > 0 {
}
// 컴파일 에러는 아니지만, gofmt 가 자동으로 괄호를 빼지는 않는다.
// 다만 관례상 쓰지 않는다.
if (x > 0) {
}
대신 중괄호 { } 는 반드시 있어야 한다.
한 줄짜리 코드라도 생략할 수 없다.
// 컴파일 에러
if x > 0 fmt.Println("yes")
// OK
if x > 0 {
fmt.Println("yes")
}
조건 앞에 명령문 넣기
Go 의 if 는 조건 앞에
짧은 명령문 하나를 같이 둘 수 있다.
if x := compute(); x > 0 {
fmt.Println("positive:", x)
} else {
fmt.Println("non-positive:", x)
}
세미콜론 ; 으로 두 부분이 나뉜다.
- 앞 부분: 변수 선언 또는 짧은 명령문
- 뒤 부분: 진짜 조건식
이 형태는 변수의 범위를 좁힌다는 장점이 있다.
x 는 if ~ else 블록 안에서만 살아 있다.
블록을 벗어나면 사라진다.
if x := compute(); x > 0 {
fmt.Println(x) // OK
}
fmt.Println(x) // 컴파일 에러: x 가 없음
변수의 범위에 대한 자세한 설명은 10장에서 다룬다.
이런 패턴은 함수가 두 값을 돌려줄 때 자주 보인다. 다중 반환값은 9장에서 본격적으로 다룬다.
8.2 switch 분기
조건이 많아지면 if / else if 가 길어진다.
이럴 때 switch 가 가독성을 살려 준다.
기본 문법
switch 값 {
case 값1:
// 값1 일 때
case 값2:
// 값2 일 때
default:
// 그 외
}
앞 절의 점수 예제를 switch 로 다시 써 본다.
grade := "B"
switch grade {
case "A":
fmt.Println("훌륭함")
case "B":
fmt.Println("좋음")
case "C":
fmt.Println("보통")
default:
fmt.Println("분발")
}
break 가 자동이다
C, Java 의 switch 는 각 case 끝에 break 를 써야 한다.
빠뜨리면 다음 case 로 흘러 넘어가는
이른바 “폴스루(fallthrough)” 현상이 생긴다.
Go 는 반대다.
각 case 끝에서 자동으로 switch 를 빠져나간다.
명시적으로 다음 case 로 넘기고 싶을 때만
fallthrough 키워드를 쓴다.
n := 1
switch n {
case 1:
fmt.Println("one")
fallthrough
case 2:
fmt.Println("two")
case 3:
fmt.Println("three")
}
실행 결과:
one
two
fallthrough 는 조건을 다시 보지 않고
그냥 다음 case 블록을 실행한다.
실제로는 거의 쓰이지 않는다.
한 case 에 여러 값
콤마로 여러 값을 한 줄에 묶을 수 있다.
switch day {
case "토", "일":
fmt.Println("주말")
case "월", "화", "수", "목", "금":
fmt.Println("평일")
}
표현식 없는 switch
switch 뒤에 비교할 값을 두지 않으면
각 case 에 자유로운 조건식을 쓸 수 있다.
긴 if / else if 사다리를 깔끔히 대체한다.
score := 85
switch {
case score >= 90:
fmt.Println("A")
case score >= 80:
fmt.Println("B")
case score >= 70:
fmt.Println("C")
default:
fmt.Println("F")
}
switch는 변수의 타입을 분기하는 용도로도 쓸 수 있다. 이를 “타입 스위치“라 부른다. 인터페이스를 배우는 16장에서 다시 만난다.
8.3 반복문 for
Go 의 반복문은 for 하나뿐이다.
다른 언어의 while, do-while 같은 키워드가 따로 없다.
대신 for 가 세 가지 모습으로 변신한다.
일반 형태 (C 스타일)
가장 익숙한 모양이다.
for 초기식; 조건식; 후처리 {
// 본문
}
예시:
for i := 0; i < 5; i++ {
fmt.Println(i)
}
실행 결과:
0
1
2
3
4
- 초기식
i := 0은 시작 직전 한 번 실행 - 조건식
i < 5가 참인 동안 반복 - 후처리
i++는 본문이 끝날 때마다 실행
조건만 (while 처럼)
초기식과 후처리를 생략하면 while 과 똑같이 쓰인다.
x := 1
for x < 100 {
x *= 2
}
fmt.Println(x) // 128
무한 루프
조건도 없으면 무한 루프가 된다.
for {
// 끝나지 않는 반복
}
break 로 빠져나오거나
return 으로 함수 자체를 끝내야 멈춘다.
for {
line := readLine()
if line == "quit" {
break
}
process(line)
}
for … range
배열, 슬라이스, 문자열, 맵 같은 컬렉션을 처음부터 끝까지 훑을 때 쓴다.
슬라이스와 맵은 11, 12장에서 자세히 다룬다. 여기서는
range의 모양만 익혀 두자.
슬라이스 순회:
nums := []int{10, 20, 30}
for i, v := range nums {
fmt.Println(i, v)
}
실행 결과:
0 10
1 20
2 30
range 는 두 값을 돌려준다.
- 첫 번째: 인덱스
- 두 번째: 그 자리의 값
값만 필요하면 인덱스를 _ 로 무시한다.
for _, v := range nums {
fmt.Println(v)
}
인덱스만 필요하면 두 번째를 아예 생략한다.
for i := range nums {
fmt.Println(i)
}
문자열 range
문자열을 range 로 돌리면
바이트가 아닌 rune (유니코드 코드 포인트) 단위로 순회한다.
s := "안녕Go"
for i, r := range s {
fmt.Printf("%d %c\n", i, r)
}
실행 결과:
0 안
3 녕
6 G
7 o
한글 한 글자가 UTF-8 로 3 바이트를 차지해서 인덱스가 1씩 증가하지 않는 점에 주목한다. 바이트 단위 순회를 원한다면 인덱스로 직접 접근해야 한다.
문자열의 바이트와 rune 차이는 6장에서 맛만 봤고, 본격적인 문자열 처리는 27장에서 다룬다.
8.4 break, continue
반복문 안에서 흐름을 가다듬는 두 키워드다.
break — 즉시 탈출
가장 안쪽 반복문을 빠져나간다.
for i := 0; i < 10; i++ {
if i == 5 {
break
}
fmt.Println(i)
}
실행 결과:
0
1
2
3
4
continue — 다음 반복으로
본문의 나머지를 건너뛰고 다음 반복으로 넘어간다.
for i := 0; i < 5; i++ {
if i%2 == 0 {
continue
}
fmt.Println(i)
}
실행 결과:
1
3
짝수일 때 continue 가 걸려서
fmt.Println 이 실행되지 않는다.
8.5 라벨로 중첩 반복 제어
break 와 continue 는 가장 안쪽 반복문만 다룬다.
중첩 반복에서 바깥 반복문을 한 번에 제어하려면
라벨 을 붙인다.
라벨 문법
OUTER:
for i := 0; i < 3; i++ {
for j := 0; j < 3; j++ {
if i*j > 3 {
break OUTER
}
fmt.Println(i, j)
}
}
- 반복문 위 줄에
라벨이름:형태로 적는다 - 라벨 이름은 대문자로 쓰는 관례가 있다
break OUTER는OUTER가 가리키는 반복문을 종료한다
실행 결과:
0 0
0 1
0 2
1 0
1 1
1 2
2 0
2 1
2 2
i*j 가 처음으로 3 을 넘는 순간 (i=2, j=2 다음, 즉 i=2, j=… 가 아니라 i=2, j=2 까지 출력 후 다음 iteration 에서 멈춘다는 점은 직접 굴려 보며 확인해 보자.)
continue 도 라벨을 받는다
OUTER:
for i := 0; i < 3; i++ {
for j := 0; j < 3; j++ {
if j == 1 {
continue OUTER
}
fmt.Println(i, j)
}
}
continue OUTER 는 안쪽 반복문을 그만두고
바깥 반복문의 다음 회차로 넘어간다.
실행 결과:
0 0
1 0
2 0
자주 쓰면 안 된다
라벨은 강력하지만, 자주 쓰면 흐름을 따라가기 어렵다. 대안이 있다면 먼저 그쪽을 고려한다.
- 깊은 중첩 자체가 신호다. 함수로 쪼개 보자.
- 함수로 빼면
return만으로 모든 반복을 한 번에 빠져나갈 수 있다.
라벨이 정말 깔끔한 답이 되는 경우만 쓴다.
8.6 정리
이 장에서 살펴본 내용:
if / else if / else로 분기를 만든다- 조건에 괄호 없음, 중괄호 필수
if x := f(); 조건 { }패턴으로 변수 범위 좁히기
switch는 자동으로 break 된다- 폴스루는
fallthrough로 명시 - 표현식 없는
switch로if사다리 대체
- 폴스루는
- 반복문은
for한 가지로 통일- C 스타일, while 스타일, 무한 루프
for ... range로 컬렉션과 문자열 순회
break,continue는 가장 안쪽 반복문에 작동- 중첩 반복 제어는 라벨로 가능하지만 자제
흐름을 다루는 도구를 갖췄으니 이제 코드 덩어리를 재사용 가능한 단위로 묶을 차례다.
다음 장에서는 함수 를 다룬다. 이름을 붙이고, 입력과 출력을 정의하고, 여러 값을 한 번에 돌려주고, defer 와 클로저까지 익힌다.